﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using Kyoh.Lib.Collections;

namespace Kyoh.Lib.IO.Ini {
    //UNDONE: SaveFile, Etc.

    /// <summary>Ini設定ファイルを解析するクラス。</summary>
    public class IniFile : ListedDictionary<string, IniSection> {

        #region 正規表現

        private static string patternSeparator = @"#;";
        private static string patternComment = @"\s*[" + patternSeparator + @"]\s*(?<comment>.*)";
        private static string patternQuote = @"""'";
        private static string patternEscape = @"(?<!\\)\\(\\\\)*";
        // ↓本来はこちらを使うべきだが、次に特定の文字を指定しているので省略
        // private static string escape = @"(?<!\\)\\(\\\\)*(?!\\)";
        private static string patternLine
            = @"\A\s*("        // +1
            + patternComment    //    <comment />

            + @"|"

            + @"\[(?<section>[^\]]*)\]" + patternComment

            + @"|"

            + @"("            // +2
            + @"(?<key>\w+)"    //                <key />
            + @"\s*=\s*"    //
            + @"("            // +3
            + @"("            // +4            (quoted)
            + @"(?<quote>[" + patternQuote + @"])"
            //+ @"(?<value>(?!(?<!" + escape + @")\k<quote>).*.)?"
            + @"(?<value>.*?)"
            + @"(?<!" + patternEscape + @")\k<quote>"
            + @")"            // -4
            + @"|"
            + @"(?<value>([^" + patternQuote + patternSeparator + @"\s][^" + patternSeparator + @"]*)?)"    //        (none-quoted)
            + @")"            // -3
            + @"(" + patternComment + @")?"        //    <comment />
            + @")"            // -2
            + @")\s*\z";    // -1

        /// <summary>Iniファイルの一行についての正規表現</summary>
        private static Regex mIniLineFormat = new Regex(patternLine);

        /// <summary>末尾にエスケープ文字(バックスラッシュ)が奇数文字続いていることのチェック</summary>
        private static Regex mEscapeBreak = new Regex(patternEscape + "$");

        private LoadIniLineHandler LoadLine = new LoadIniLineHandler(LoadLineWithoutEscape);

        #endregion

        /// <summary><see cref="IniFile"/> クラスの新しいインスタンスを初期化します。</summary>
        public IniFile() : base() { }

        /// <summary><see cref="IniSection"/> クラスの新しいインスタンスを追加します。</summary>
        /// <param name="sectionName">追加するセクション名。</param>
        /// <returns>追加した <see cref="IniSection"/> オブジェクト。</returns>
        public IniSection Add(string sectionName) {
            IniSection ret = new IniSection();
            Add(sectionName, ret);
            return ret;
        }

        #region I/O設定項目

        /// <summary>ファイルの行末端文字を、バックスラッシュ（&#x005C;）によりエスケープするかどうかを取得・設定します。</summary>
        public bool EscapeLineBreak {
            get {
                return mEscapeLineBreak;
            }
            set {
                mEscapeLineBreak = value;
                if (mEscapeLineBreak)
                    LoadLine = new LoadIniLineHandler(LoadLineWithEscape);
                else
                    LoadLine = new LoadIniLineHandler(LoadLineWithoutEscape);
            }
        }

        private bool mEscapeLineBreak = false;

        /// <summary>ファイルへの読み込み・書き込みに使用する文字エンコーディングを取得・設定します。</summary>
        public Encoding Encoding { get; set; }

        /// <summary>ファイルへの読み込み・書き込み時にファイルの先頭にあるバイト順序マークを検索するかどうかを取得・設定します。</summary>
        public bool DetectEncodingFromByteOrderMarks { get; set; }

        /// <summary>ファイルへの読み込み・書き込みに使用するバッファの最小サイズを取得・設定します。</summary>
        public int BufferSize { get; set; }

        #endregion

        #region LoadFile (オーバーロード)

        ///<summary>指定したストリームから、Iniファイル設定項目を読み込みます。</summary>
        ///<param name="stream">読み込まれるストリーム。</param>
        ///<exception cref="ArgumentException">stream が読み取りをサポートしていません。</exception>
        ///<exception cref="ArgumentNullException">stream が null です。</exception>
        public void LoadFile(Stream stream) {
            using (StreamReader reader = new StreamReader(stream, Encoding, DetectEncodingFromByteOrderMarks, BufferSize))
                LoadFile(reader);
        }

        ///<summary>指定したファイルから、Iniファイル設定項目を読み込みます。</summary>
        ///<param name="path">読み込まれる完全なファイルパス。</param>
        ///<exception cref="ArgumentException">path が空の文字列 ("") です。</exception>
        ///<exception cref="ArgumentNullException">path が null です。</exception>
        ///<exception cref="FileNotFoundException">ファイルが見つかりません。</exception>
        ///<exception cref="DirectoryNotFoundException">割り当てられていないドライブであるなど、指定されたパスが無効です。</exception>
        ///<exception cref="IOException">path に、ファイル名、ディレクトリ名、またはボリューム ラベルとしては不正または無効な構文が含まれています。</exception>
        public void LoadFile(string path) {
            using (StreamReader reader = new StreamReader(path, Encoding, DetectEncodingFromByteOrderMarks, BufferSize))
                LoadFile(reader);
        }

        #endregion

        #region LoadFile

        /// <summary>
        /// 指定されたSystem.IO.TextReaderから読み込んだIniファイル設定項目をインスタンスに設定します。
        /// </summary>
        /// <param name="reader">読み込みを行うSystem.IO.TextReader</param>
        private void LoadFile(TextReader reader) {
            Match match;
            IniSection cursor = null;
            string iniLine;

            Clear();

            try {

                while (null != (iniLine = LoadLine(reader))) {

                    iniLine = iniLine.Trim();

                    if (string.IsNullOrEmpty(iniLine))
                        continue;

                    match = mIniLineFormat.Match(iniLine);

                    if (match.Groups["section"].Success) {
                        cursor = new IniSection();
                        Add(match.Groups["section"].Value, cursor);
                        //HACK: コメントの扱い。
                        //comment = match.Groups["comment"].Value;
                    }
                    else if (match.Groups["key"].Success) {
                        if (cursor == null)
                            throw new IniFileFormatException("Iniファイルの書式が不正です。セクションが適切に設定されていません。" + iniLine);
                        cursor.Add(match.Groups["key"].Value, match.Groups["value"].Value);
                        //HACK: コメントの扱い。
                        //comment = match.Groups["comment"].Value;
                    }
                    else if (match.Groups["comment"].Success) {
                        //HACK: コメントの扱い。
                        //comment = match.Groups["comment"].Value;
                        continue;
                    }
                    else
                        throw new IniFileFormatException("Iniファイルの書式が不正です。" + iniLine);
                }
            }
            catch (IniFileFormatException) {
                throw;
            }
            catch (IOException) {
                throw;
            }
            catch (Exception e) {
                throw new IniFileFormatException("Iniファイルの書式が不正です。", e);
            }
        }

        /// <summary>
        /// 改行文字のエスケープを行って、論理行を読み込みます。
        /// </summary>
        /// <param name="reader">読み込みを行うSystem.IO.TextReader</param>
        /// <returns>読み込んだ論理行</returns>
        private static string LoadLineWithEscape(TextReader reader) {
            StringBuilder builder = new StringBuilder();
            string line;
            bool match;
            bool returnNull = true;

            do {

                line = reader.ReadLine();
                if (line == null)
                    break;

                // 末尾のエスケープ文字を処理
                match = mEscapeBreak.IsMatch(line);
                if (match)
                    line = line.Substring(0, line.Length - 1);

                returnNull = false;
                builder.Append(line);

            } while (match);

            return returnNull ? null : builder.ToString();
        }

        /// <summary>
        /// 改行文字のエスケープを行わずに、論理行を読み込みます。
        /// </summary>
        /// <param name="reader">読み込みを行うSystem.IO.TextReader</param>
        /// <returns>読み込んだ論理行</returns>
        private static string LoadLineWithoutEscape(TextReader reader) {
            return reader.ReadLine();
        }

        #endregion

        #region Parse

        /// <summary>Ini設定文字列を読み込んで、設定ファイルの中身をこのインスタンスに反映します。</summary>
        /// <param name="iniString">Ini設定ファイルの中身を表す文字列。</param>
        public void Parse(string iniString) {
            using (StringReader reader = new StringReader(iniString)) {
                LoadFile(reader);
            }
        }

        #endregion

        #region SaveFile

        ///<summary>指定したストリームに、Iniファイル設定項目を書き込みます。</summary>
        ///<param name="stream">書き込むストリーム。</param>
        ///<exception cref="ArgumentException">stream が読み取りをサポートしていません。</exception>
        ///<exception cref="ArgumentNullException">stream が null です。</exception>
        public void SaveFile(Stream stream) {
            using (StreamWriter writer = new StreamWriter(stream, Encoding, BufferSize))
                SaveFile(writer);
        }

        ///<summary>指定したファイルに、Iniファイル設定項目を書き込みます。</summary>
        ///<param name="path">書き込む完全なファイルパス。</param>
        ///<exception cref="ArgumentException">path が空の文字列 ("") です。</exception>
        ///<exception cref="ArgumentNullException">path が null です。</exception>
        ///<exception cref="FileNotFoundException">ファイルが見つかりません。</exception>
        ///<exception cref="DirectoryNotFoundException">割り当てられていないドライブであるなど、指定されたパスが無効です。</exception>
        ///<exception cref="IOException">path に、ファイル名、ディレクトリ名、またはボリューム ラベルとしては不正または無効な構文が含まれています。</exception>
        public void SaveFile(string path) {
            using (StreamWriter writer = new StreamWriter(path, false, Encoding, BufferSize))
                SaveFile(writer);
        }
        
        /// <summary>指定されたSystem.IO.StreamWriterに現在のインスタンスのIniファイル設定項目を書き込みます。</summary>
        /// <param name="writer">書き込みを行う <see cref="TextWriter"/></param>
        private void SaveFile(TextWriter writer) {
            foreach (KeyValuePair<string, IniSection> sec in this) {
                if (checkSection(sec.Key) == false)
                    throw new IniFileFormatException("Iniファイルの書式が不正です。セクション名に']'を含むファイルは作成できません。");
                writer.WriteLine("[" + sec.Key + "]");
                foreach (KeyValuePair<string, string> pro in sec.Value) {
                    if (checkKey(pro.Key) == false)
                        throw new IniFileFormatException("Iniファイルの書式が不正です。セクション名は英数字とアンダースコアのみで構成されている必要があります。");
                    writer.WriteLine(pro.Key + "=\"" + QuoteValue(pro.Value) + "\"");
                }
            }
        }

        private bool checkSection(string sectionName) {
            return !sectionName.Contains("]");
        }
        private bool checkKey(string key) {
            return checkKey(key, 0);
        }

        private bool checkKey(string key, int start) {
            return start >= key.Length || (char.IsLetterOrDigit(key[start]) || key[start] == '_' && checkKey(key, start + 1));
        }

        private bool checkValue(string value) {
            return !value.StartsWith("\"")
                && !value.StartsWith("'")
                && !value.Contains("#")
                && !value.Contains(";");
        }

        private string QuoteValue(string value) {
            if (checkValue(value))
                return value;
            value.Replace("\\", "\\\\");
            value.Replace("\"", "\\\"");
            return "\"" + value + "\"";
        }

        #endregion

    }

}